#!pip install "pandas<2.0.0"
#!pip install plotly
#!pip install folium
#!pip install geopy
#!pip install nbformat Auswertung von Daten der Deutschen Bahn
Github: https://github.com/lz039/python4ds_project
Einleitung
Schienennetz vor dem Kollaps - so titeln Marie Blöcher, Nils Naber und Isabel Schneider vom NDR. Die Deutsche Bahn habe zwar ehrgeizige Ziele, allerdings ist jahrelang zu wenig Geld ins Netz geflossen.
Rund 60 Milliarden Euro müssten laut DB ausgegeben werden, um alle Probleme im Netz zu beheben, die sich über die vergangenen Jahre angesammelt haben. Der Zustand von Strecken und Gleisen wurde über viele Jahre vernachlässigt, sagt Bahnexperte Christian Böttger.
Die Bahn steht aktuell in keinem guten Licht. Zu viele Verspätungen, Zugausfälle und marode Infrastruktur.
Doch wie steht es wirklich um den Zustand der Bahn?
In dieser Analyse wird auf Daten der Deutschen Bahn zugegriffen, um dieser Frage auf den Grund zu gehen.
Die Bahn stellt über den API Marketplace eine Fülle an Daten zur Verfügung.
Die meisten Daten stehen kostenlos zur Verfügung, für einige sind kostenpflichtige Pläne nötig.
Einige APIs liefern ähnliche Funktionalität nur in anderem Format oder noch detaillierter, z.B. RIS::Stations und StaDa. Im Rahmen dieses Projekts werden folgende Daten betrachtet:
- Als Grundlage dient RIS::Stations, worüber sich alle deutschen Bahnhöfe abrufen lassen.
- FaSta - Station Facility Status gibt Auskunft über den Zustand der Bahnhöfe
- Railway-Stations Pictures ermöglich den Zugriff auf Bilder jedes einzelnen Bahnhofs.
- Facility Stations sind alle Dienstleistungen rund um die Bahnhöfe.
Daten abrufen
Über folgende APIs werden die Daten im JSON-Format abgerufen, in pandas dataframes umgewandelt und gespeichert.
url_parking = 'https://apis.deutschebahn.com/db-api-marketplace/apis/parking-information/db-bahnpark/v2/'
url_ris_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/ris-stations/v1/'
url_rw_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/api.railway-stations.org/photoStationById/'
url_facility_stations = 'https://apis.deutschebahn.com/db-api-marketplace/apis/fasta/v2/stations/'
Häufig können nicht alle Daten auf einmal abgerufen werden, weshalb mehrere Aufrufe gemacht werden müssen. Anschließend ist ein mapping der JSON Bahn-API-Datenstruktur auf ein flaches Data Frame nötig.
Da der Abruf der Daten einige Minuten dauert, ist dieser und die eigentliche Auswertung in zwei verschiedenen files getrennt.
Imports
Es werden Dateien im Pickel-Format genutzt, um ganze Pandas Dataframes zwischen dem Scraping-Prozess und der Datenauswertung auszutauschen.
Der Vorteil dabei ist, dass die Struktur und die Datenformate beibehalten werden.
Für die Visualisierung wird Plotly und Folium genutzt.
Da für gewisse Funktionen Python 3.10.1 benötigt wurde, bringt das Projekt eine eigene virtuelle Python Umgebung mit. Allerdings werden nur die folgenden Pakete benötigt
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import pickle
import plotly.express as px
import plotly.graph_objects as go
import folium
import folium.plugins as plugins
from geopy.geocoders import NominatimDaten laden
Hier werden die Daten aus den Pickel-Files in Dataframes geladen. Da diese Funktionalität häufig benötigt wird, ist sie in eine Funktion ausgelagert. Die Daten aus dem Scraping liegen im data Ordner.
data_folder = 'data/'def loadData(fileName):
with open(data_folder + fileName, 'rb') as pkl_file:
return pickle.load(pkl_file)df_stations = loadData('stations.pkl')
df_stopplaces = loadData('stopplaces_new.pkl')
df_facilities = loadData('station_facilities.pkl')Es kann zwar mehrere Bilder pro Bahnhof geben, dies wurde beim data scraping allerdings bereits berücksichtigt, sodass immer genau ein Bild pro Bahnhof vorhanden ist.
Die Bilder sind noch als Dictionary gespeichert, sodass dies noch in ein Pandas DataFrame umgewandelt werden muss.
df_station_images = loadData('station_images.pkl')
df_images = pd.DataFrame.from_dict({k: v for k, v in df_station_images.items() if v}).T
df_images.columns = ['image']
df_images = df_images.reset_index()Die verschiedenen Daten passen von der Anzahl nicht komplett zusammen.
Es ist aber durchaus erklärbar, dass es mehr Einrichtungen und Haltestellen als tatsächliche Bahnhöfe bzw. Bahnhofsgebäude gibt. Wie genau die Daten aussehen, wird im Folgenden geprüft.
print(f'Stations: {df_stations.shape}')
print(f'Station images: {df_images.shape}')
print(f'Facilities: {df_facilities.shape}')
print(f'Stopplaces: {df_stopplaces.shape}')Stations: (5690, 16)
Station images: (5627, 2)
Facilities: (3550, 6)
Stopplaces: (5727, 9)
Bahnhöfe
Das Data Frame der Bahnhöfe enthält alle Haltestellen der Deutschen Bahn in Deutschland.
Jeder Bahnhof hat eine eindeutige id. Zusätzlich werden beispielsweise der Name, die Adresse und Geo-Koordinaten mitgeliefert.
df_stations.head(3)| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
Bilder von Bahnhöfen
Nutzer können Bilder zu Bahnhöfen hochladen. Das Data Frame enthält Links zu diesen Bildern. Die Spalte index referenziert die Spalte id der Bahnhöfe.
df_images.head(3)| index | image | |
|---|---|---|
| 0 | 1 | https://api.railway-stations.org/photos/de/1_1... |
| 1 | 1000 | https://api.railway-stations.org/photos/de/100... |
| 2 | 1001 | https://api.railway-stations.org/photos/de/100... |
Hier ein Beispiel vom Bahnhof in Aachen.
Einrichtungen
Mit Einrichtungen sind beispielsweise Geräte wie Aufzüge auf Bahnhöfen gemeint.
Das interessante ist, dass auch die Funktionstüchtigkeit der Einrichtung mitgeliefert wird.
df_facilities.head(3)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1 | 1 | zu Gleis 2/3 | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2 | 1 | zu Gleis 6/7 | DB Station&Service | ACTIVE | available | ELEVATOR |
In Wahrheit gibt es genau zwei verschiedene Arten von Einrichtungen: Aufzüge und Rolltreppen.
df_facilities['type'].unique()array(['ELEVATOR', 'ESCALATOR'], dtype=object)
Haltestellen
Haltestellen enthalten viele Informationen zu den Bahnhöfen, zusätzlich auch die Art der Transportmittel und welcher Verkehrsbund gilt.
df_stopplaces.head(3)| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | DE | NW | Europe/Berlin | 50.767800 | 6.091499 |
| 1 | 1000 | Burkhardswalde-Maxen | [REGIONAL_TRAIN] | [VVO] | DE | SN | Europe/Berlin | 50.925146 | 13.838369 |
| 2 | 1001 | Burkhardtsdorf | [REGIONAL_TRAIN, BUS] | [VMS] | DE | SN | Europe/Berlin | 50.733196 | 12.932137 |
Explorative Datenanalyse
Datentypen
Als erstes wird geprüft, ob die Data Frames korrekte Datentypen haben und werden bei Bedarf korrigiert.
Die Spalte id soll immer vom Typ int sein, um sie später besser zusammenführen zu können.
df_stations['id'] = df_stations['id'].astype(int)
df_stations.dtypesid int32
name object
metropolis object
street object
houseNumber object
postalCode object
city object
state object
country object
stationCategory object
owner object
organisationalUnit object
countryCode object
latitude float64
longitude float64
timeZone object
dtype: object
df_stopplaces['id'] = df_stopplaces['id'].astype(int)
df_stopplaces.dtypesid int32
name object
availableTransports object
transportAssociations object
countryCode object
state object
timeZone object
latitude float64
longitude float64
dtype: object
df_facilities['id'] = df_facilities['id'].astype(int)
df_facilities.dtypesid int32
description object
operatorname object
state object
stateExplanation object
type object
dtype: object
df_images['index'] = df_images['index'].astype(int)
df_images.dtypesindex int32
image object
dtype: object
Fehlende Werte
Als nächstes wird auf fehlende Werte geprüft, um zu prüfen, ob Korrekturen notwendig sind.
df_stations.isna().sum()id 0
name 0
metropolis 0
street 8
houseNumber 893
postalCode 7
city 4
state 0
country 0
stationCategory 12
owner 0
organisationalUnit 0
countryCode 0
latitude 282
longitude 282
timeZone 0
dtype: int64
Das Hausnummernfeld fehlt sehr oft. Da diese Information aber nicht genutzt wird, stellt dies kein Problem dar.
Die Angaben für Latitude und Longitude fehlen ebenfalls häufig. Es kann über die Adresse versucht werden, die fehlenden Werte herauszufinden.
Da das Nachschauen der Werte einige Zeit in Anspruch nimmt, ist dieser Code in der finalen Abgabe auskommentiert.
Grundsätzlich wird aber anhand der Spalten postalCode, city, state und country mithilfe des Pakets aus der Vorlesung geopy versucht, die Geo-Koordinaten aufzulösen.
# geolocator = Nominatim(user_agent="my_app")
# filtered_rows = df_stations[df_stations['latitude'].isnull()]
# result = {}
# # Print the entire row for each entry with NaN latitude
# for index, row in filtered_rows.iterrows():
# try:
# address = f'{row["postalCode"]} {row["city"]} {row["state"]} {row["country"]}'
# result[row['id']] = geolocator.geocode(address)
# except:
# pass# dict={}
# # extract lat/lon
# for index, entry in result.items():
# if entry:
# dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
# geocode_result = pd.DataFrame().from_dict(dict).T
# geocode_result['id'] = geocode_result.index
# geocode_result['id'] = geocode_result['id'].astype(int)Die Ergebnisse werden in einem Pickel-File gespeichert und daraus wieder geladen.
# output = open(data_folder + 'manual_geocode_results.pkl', 'wb')
# pickle.dump(geocode_result, output)
# output.close()geocode_result = loadData('manual_geocode_results.pkl')df_stations| id | name | metropolis | street | houseNumber | postalCode | city | state | country | stationCategory | owner | organisationalUnit | countryCode | latitude | longitude | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen Hbf | {} | Bahnhofstr. | 2a | 52064 | Aachen | Nordrhein-Westfalen | DE | CATEGORY_2 | DB S&S | RB West | DE | 50.767800 | 6.091499 | Europe/Berlin |
| 1 | 1000 | Burkhardswalde-Maxen | {} | Gesundbrunnen | 60c | 01809 | Müglitztal-Burkhardswalde | Sachsen | DE | CATEGORY_7 | DB S&S | RB Südost | DE | 50.925146 | 13.838369 | Europe/Berlin |
| 2 | 1001 | Burkhardtsdorf | {} | Bahnhofstraße | NaN | 09235 | Burkhardtsdorf | Sachsen | DE | CATEGORY_6 | DB Regio-Netze | Erzgebirgsbahn (EGB) | DE | NaN | NaN | Europe/Berlin |
| 3 | 1002 | Bürstadt | {} | Bahnhofsallee | 17 | 68642 | Bürstadt | Hessen | DE | CATEGORY_6 | DB S&S | RB Mitte | DE | 49.645769 | 8.458188 | Europe/Berlin |
| 4 | 1005 | Buschow | {} | Bahnhofstr. | 28 | 14715 | Märkisch Luch OT Buschow | Brandenburg | DE | CATEGORY_6 | DB S&S | RB Ost | DE | 52.592203 | 12.628996 | Europe/Berlin |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5685 | 995 | Burgstädt | {} | Bahnhofstr. | 1 | 09217 | Burgstädt | Sachsen | DE | CATEGORY_6 | DB S&S | RB Südost | DE | 50.915817 | 12.812707 | Europe/Berlin |
| 5686 | 996 | Burgstall (Murr) | {} | Bahnhofstr. | 1 | 71576 | Burgstetten | Baden-Württemberg | DE | CATEGORY_6 | DB S&S | RB Südwest | DE | 48.928647 | 9.369932 | Europe/Berlin |
| 5687 | 997 | Steinfurt-Burgsteinfurt | {} | Bahnhofsplatz | 6 | 48565 | Steinfurt-Burgsteinfurt | Nordrhein-Westfalen | DE | CATEGORY_6 | DB S&S | RB West | DE | 52.147384 | 7.329340 | Europe/Berlin |
| 5688 | 998 | Burgthann | {} | Bahnhofstr. | 40 | 90559 | Burgthann | Bayern | DE | CATEGORY_5 | DB S&S | RB Süd | DE | 49.342474 | 11.309307 | Europe/Berlin |
| 5689 | 999 | Regensburg-Burgweinting | {} | Alfons-Goppel-Straße | NaN | 93055 | Regensburg | Bayern | DE | CATEGORY_6 | DB S&S | RB Süd | DE | 48.990725 | 12.146486 | Europe/Berlin |
5690 rows × 16 columns
Nun liegen die vorhanden Geo-Koordinaten im Data Frame df_stations und die neu ermittelten in geocode_result.
Um nun das Ergebnis aus beiden Tabellen zu bekommen, wird die Funktion combine_first genutzt.
df_stations = df_stations.set_index('id').combine_first(geocode_result.set_index('id')).reset_index()Anstelle von 282 fehlenden Werten sind es jetzt nur noch 24!
Die restlichen Werte werden aufgrund der geringen Anzahl ignoriert.
df_stations.isna().sum()id 0
city 4
country 0
countryCode 0
houseNumber 893
latitude 24
longitude 24
metropolis 0
name 0
organisationalUnit 0
owner 0
postalCode 7
state 0
stationCategory 12
street 8
timeZone 0
dtype: int64
df_stopplaces.isna().sum()id 0
name 0
availableTransports 0
transportAssociations 0
countryCode 0
state 11
timeZone 0
latitude 0
longitude 0
dtype: int64
Die Haltestellen scheinen eine bessere Datenqualität zu haben. Daher resultieren hieraus keine Probleme, die betrachtet werden müssen.
df_facilities.isna().sum()id 0
description 51
operatorname 0
state 0
stateExplanation 0
type 0
dtype: int64
Einige Beschreibungen sind leer. Diese beschreiben meist, wohin der Aufzug oder die Rolltreppe führen, z.B. “zu Gleis 1”. Aus dieser Information lässt sich nichts ableiten, somit sind die leeren Felder zu vernachlässigen.
Das Gute ist, dass weder die Beschreibung type noch state jemals leer ist.
df_facilities.head(1)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
df_facilities[df_facilities['description'].isna()]| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 4 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 5 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 6 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 7 | 1 | None | DB Station&Service | INACTIVE | under construction | ELEVATOR |
| 274 | 1390 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 375 | 161 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 630 | 1866 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 648 | 1866 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 660 | 1867 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 661 | 1867 | None | DB Station&Service | UNKNOWN | monitoring disrupted | ELEVATOR |
| 662 | 1867 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 676 | 1877 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 687 | 1907 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 688 | 1907 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 794 | 220 | None | DB Station&Service | INACTIVE | under construction | ESCALATOR |
| 861 | 2420 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 862 | 2420 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1010 | 2545 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1024 | 2551 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1042 | 2618 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1043 | 2618 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1133 | 2827 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1462 | 3631 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1752 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1755 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1756 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1761 | 4234 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 1775 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1776 | 4234 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1783 | 4236 | None | DB Station&Service | UNKNOWN | monitoring not available | ELEVATOR |
| 1828 | 4242 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 1848 | 4258 | None | DB Station&Service | UNKNOWN | monitoring disrupted | ESCALATOR |
| 2007 | 4587 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2026 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2027 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2028 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2035 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2036 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2037 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2041 | 4593 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2176 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2177 | 4809 | None | DB Station&Service | INACTIVE | not available | ESCALATOR |
| 2178 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2179 | 4809 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 2763 | 5844 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2764 | 5844 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 2840 | 6071 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 3064 | 6612 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 3065 | 6612 | None | DB Station&Service | ACTIVE | available | ELEVATOR |
| 3408 | 8097 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
| 3409 | 8097 | None | DB Station&Service | ACTIVE | available | ESCALATOR |
Daten zusammen bringen
Jetzt können alle Daten pro Bahnhofsstation zusammengebracht werden.
Dazu werden zunächst alle doppelten Spalten entfernt und dann die Tabellen mithilfe zweier merge zusammengefügt.
Davor wird nochmal stichprobenartig geprüft, ob die IDs auch wirklich zusammenpassen.
Die Facilities können dabei nicht gejoined werden, da ein Bahnhof in der Regel mehrere davon aufweist (1:n Beziehung)
df_stopplaces[df_stopplaces['name'] == 'Ahrensfelde']| id | name | availableTransports | transportAssociations | countryCode | state | timeZone | latitude | longitude | |
|---|---|---|---|---|---|---|---|---|---|
| 1539 | 28 | Ahrensfelde | [REGIONAL_TRAIN] | [VBB] | DE | BE | Europe/Berlin | 52.571375 | 13.565154 |
df_stations[df_stations['name'] == 'Ahrensfelde']| id | city | country | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | timeZone | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 23 | 28 | Berlin | DE | DE | NaN | 52.571375 | 13.565154 | {} | Ahrensfelde | RB Ost | DB S&S | 12689 | Berlin | CATEGORY_4 | Märkische Allee | Europe/Berlin |
df_stopplaces.drop(columns=['name', 'state', 'countryCode', 'latitude', 'longitude','timeZone'], inplace=True)
df = pd.merge(df_stations, df_stopplaces, on='id', how='left')
df = pd.merge(left=df, right=df_images, left_on=['id'], right_on=['index'], how='left')
df.drop(columns=['timeZone','index','country'], inplace=True)df.isna().sum()id 0
city 4
countryCode 0
houseNumber 903
latitude 24
longitude 24
metropolis 0
name 0
organisationalUnit 0
owner 0
postalCode 7
state 0
stationCategory 12
street 8
availableTransports 18
transportAssociations 18
image 64
dtype: int64
Es fehlen nun noch einzelne Werte, aber mit dieser Datengrundlage kann gut gearbeitet werden.
Datenvisualisierung
Anhand der Daten ergeben sich unterschiedliche Fragen:
- Sind tatsächlich nur deutsche Stationen vorhanden?
- Wie sind die Betreiber
ownerund VerkehrsverbündeorganisationalUnitausgeprägt?
- Wie sind die Betreiber
- Wie verteilen sich die Stationen innerhalb von Deutschland (Geo-Koordinaten anschauen)
- Wie sind die Transportmittel
transportAssociationsausgeprägt?
- Wie sind die Transportmittel
- Wofür stehen die Werte von
stationCategory?
- Wofür stehen die Werte von
df.head(1)| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... |
a) Bahnhöfe der Schweiz
Es gibt im Datenbestand auch einige Bahnhöfe, die in der Schweiz liegen.
Schaffhausen ist ein Gemeinschaftsbahnhof zwischen der Schweizerischen Bundesbahnen und dem deutschen Bundeseisenbahnvermögen. Quelle: Wikipedia
Auffällig ist, dass bei diesen Bahnhöfen keine Bilder und auch keine stationCategory / transportAssociations vorhanden sind. Dies scheint nur im Datenbestand der deutschen Bahnhöfe zu existieren.
df[df['countryCode']!='DE']| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 347 | 424 | Basel | CH | 200 | 47.567288 | 7.607805 | {} | Basel Bad Bf | RB Südwest | DB S&S | 4016 | Schweiz CH | NaN | Schwarzwaldallee | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [RVL] | NaN |
| 2093 | 2698 | Schaffhausen | CH | 1 | 47.717003 | 8.664127 | {} | Herblingen | RB Südwest | DB S&S | 8207 | Schweiz CH | NaN | Bruderhalde | [CITY_TRAIN] | [] | NaN |
| 3368 | 4399 | Neuhausen | CH | 18 | 47.682615 | 8.612186 | {} | Neuhausen Bad Bf | RB Südwest | DB S&S | 8212 | Schweiz CH | NaN | Badischen Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 3387 | 4424 | Neunkirch | CH | 3 | 47.689151 | 8.495384 | {} | Neunkirch | RB Südwest | DB S&S | 8225 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 4053 | 5274 | Riehen | CH | 25 | 47.583157 | 7.652014 | {} | Riehen | RB Südwest | DB S&S | 4125 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [RVL] | NaN |
| 4233 | 5530 | Schaffhausen | CH | 29 | NaN | NaN | {} | Schaffhausen | RB Südwest | DB S&S | 8200 | Schweiz CH | NaN | Bahnhofstr. | NaN | NaN | NaN |
| 4715 | 6192 | Thayngen | CH | 31 | 47.745502 | 8.704300 | {} | Thayngen | RB Südwest | DB S&S | 8240 | Schweiz CH | NaN | Bahnhofstr. | [CITY_TRAIN] | [] | NaN |
| 4743 | 6235 | Trasadingen | CH | 1 | 47.665238 | 8.436804 | {} | Trasadingen | RB Südwest | DB S&S | 8219 | Schweiz CH | NaN | Bahnhofstr. | [REGIONAL_TRAIN] | [] | NaN |
| 5128 | 6762 | Wilchingen | CH | 18 | 47.679448 | 8.463860 | {} | Wilchingen-Hallau | RB Südwest | DB S&S | 8217 | Schweiz CH | NaN | Bahnhofstrasse | [REGIONAL_TRAIN] | [] | NaN |
b) Betreiber und Verkehrsbünde
def plot_counts(column):
counts = df[column].value_counts().reset_index()
counts.columns = [column, 'count']
fig = px.bar(counts, x=column, y='count', barmode='group', text='count')
fig.show()Die meisten Bahnhöfe gehören der DB Station&Service AG. Laut ihrer Webseite, unterhalten sie rund 5.400 Bahnhöfe.
Das können wir bestätigen! The DB S&S hat laut den Daten 5.413 Bahnhöfe. Der Rest, 277, werden von der DB Regio-Netze unterhalten.
plot_counts('owner')Unable to display output for mime type(s): application/vnd.plotly.v1+json
Betrachtet man die Organisationsbereiche, gibt es viele Stationen in der Mitte, im Süden und im Westen von Deutschland. Der Norden und Osten liegen hingegen auf den letzten Plätzen.
Es gibt auch einige kleinere Organisationseinheiten für spezielle Regionen.
plot_counts('organisationalUnit')Unable to display output for mime type(s): application/vnd.plotly.v1+json
Tatsächlich ist zu erkennen, dass die führenden Bundesländer Bayern, Baden-Württemberg und Nordrhein-Westfalen (NRW) sind.
Es besteht eine deutliche Lücke zwischen ihnen und dem viertplatzierten Bundesland Hessen. Natürlich muss bei der Anzahl der Bahnhöfe aber auch die Größe der Bundesländer berücksichtigt werden.
Daher wird geprüft, welches Bundesland laut seiner Größe die meisten Bahnhöfe hat. Dazu kann die Größe der Bundesländer abgerufen und ins Verhältnis mit der Anzahl der Stationen gesetzt werden.
c) Bahnhofsdichte in Deutschland
Größe der Bundesländer:
df_states = pd.read_csv(data_folder + 'states_size.csv', sep=';')
df_states['size'] = df_states['size'].astype(float)
df_states| state | size | |
|---|---|---|
| 0 | Baden-Württemberg | 35747.82 |
| 1 | Bayern | 70541.57 |
| 2 | Berlin | 891.12 |
| 3 | Brandenburg | 29654.35 |
| 4 | Bremen | 419.62 |
| 5 | Hamburg | 755.09 |
| 6 | Hessen | 21115.64 |
| 7 | Mecklenburg-Vorpommern | 23295.45 |
| 8 | Niedersachsen | 47709.82 |
| 9 | Nordrhein-Westfalen | 34112.44 |
| 10 | Rheinland-Pfalz | 19858.00 |
| 11 | Saarland | 2571.11 |
| 12 | Sachsen | 18449.93 |
| 13 | Sachsen-Anhalt | 20459.12 |
| 14 | Schleswig-Holstein | 15804.30 |
| 15 | Thüringen | 16202.39 |
| 16 | Deutschland | 357587.77 |
Anzahl Bahnhöfe pro Bundesland:
df_grp_states = pd.DataFrame(df_stations.groupby(by='state').count()['id'].sort_values(ascending=False))
df_grp_states.rename(columns={'id': 'count'}, inplace=True)
df_grp_states| count | |
|---|---|
| state | |
| Bayern | 1025 |
| Baden-Württemberg | 720 |
| Nordrhein-Westfalen | 711 |
| Hessen | 479 |
| Sachsen | 478 |
| Rheinland-Pfalz | 419 |
| Niedersachsen | 357 |
| Brandenburg | 310 |
| Sachsen-Anhalt | 289 |
| Thüringen | 289 |
| Mecklenburg-Vorpommern | 180 |
| Schleswig-Holstein | 137 |
| Berlin | 133 |
| Saarland | 77 |
| Hamburg | 58 |
| Bremen | 16 |
| Schweiz CH | 12 |
df_germany = pd.DataFrame(index=['count'], data={
'Deutschland':df_grp_states.sum().values[0]
}).T
df_grp_states = pd.concat([df_grp_states, df_germany])
df_grp_states['state'] = df_grp_states.index
df_grp_states = pd.merge(df_states, df_grp_states, how='left', on='state')df_grp_states| state | size | count | |
|---|---|---|---|
| 0 | Baden-Württemberg | 35747.82 | 720 |
| 1 | Bayern | 70541.57 | 1025 |
| 2 | Berlin | 891.12 | 133 |
| 3 | Brandenburg | 29654.35 | 310 |
| 4 | Bremen | 419.62 | 16 |
| 5 | Hamburg | 755.09 | 58 |
| 6 | Hessen | 21115.64 | 479 |
| 7 | Mecklenburg-Vorpommern | 23295.45 | 180 |
| 8 | Niedersachsen | 47709.82 | 357 |
| 9 | Nordrhein-Westfalen | 34112.44 | 711 |
| 10 | Rheinland-Pfalz | 19858.00 | 419 |
| 11 | Saarland | 2571.11 | 77 |
| 12 | Sachsen | 18449.93 | 478 |
| 13 | Sachsen-Anhalt | 20459.12 | 289 |
| 14 | Schleswig-Holstein | 15804.30 | 137 |
| 15 | Thüringen | 16202.39 | 289 |
| 16 | Deutschland | 357587.77 | 5690 |
- Gemäß seiner Größe hat Berlin die meisten Haltestellen, wobei jede Station durchschnittlich etwa 6,7 km² abdeckt. Für eine Hauptstadt ergibt dies Sinn.
- Im Allgemeinen haben alle großen Städte (Hamburg, Bremen) dieses gute Verhältnis.
- Zuvor waren Bayern und Baden-Württemberg die Regionen mit den meisten Stationen, nun befinden sie sich auf den Plätzen 11 und 8.
- Das Saarland, das die wenigsten Bahnhöfe hat, weist das beste Verhältnis der Fläche zu Stationen unter den Bundesländern auf.
- In Niedersachsen muss eine Station im Durchschnitt etwa 133,6 km² abdecken. Wenn man diese Strecke dahingehen auslegt, dass diese mit dem Auto zurückgelegt werden muss, um zur nächstgelegenen Bahnstation zu gelangen, ist das sehr viel.
- Der Durchschnitt für ganz Deutschland liegt bei 62 km².
df_grp_states['ratio'] = df_grp_states['size']/df_grp_states['count']
df_grp_states.sort_values('ratio', ascending=True).reset_index().drop(columns=['index'])| state | size | count | ratio | |
|---|---|---|---|---|
| 0 | Berlin | 891.12 | 133 | 6.700150 |
| 1 | Hamburg | 755.09 | 58 | 13.018793 |
| 2 | Bremen | 419.62 | 16 | 26.226250 |
| 3 | Saarland | 2571.11 | 77 | 33.391039 |
| 4 | Sachsen | 18449.93 | 478 | 38.598180 |
| 5 | Hessen | 21115.64 | 479 | 44.082756 |
| 6 | Rheinland-Pfalz | 19858.00 | 419 | 47.393795 |
| 7 | Nordrhein-Westfalen | 34112.44 | 711 | 47.978115 |
| 8 | Baden-Württemberg | 35747.82 | 720 | 49.649750 |
| 9 | Thüringen | 16202.39 | 289 | 56.063633 |
| 10 | Deutschland | 357587.77 | 5690 | 62.844951 |
| 11 | Bayern | 70541.57 | 1025 | 68.821044 |
| 12 | Sachsen-Anhalt | 20459.12 | 289 | 70.792803 |
| 13 | Brandenburg | 29654.35 | 310 | 95.659194 |
| 14 | Schleswig-Holstein | 15804.30 | 137 | 115.359854 |
| 15 | Mecklenburg-Vorpommern | 23295.45 | 180 | 129.419167 |
| 16 | Niedersachsen | 47709.82 | 357 | 133.640952 |
Um diese Informationen auf einer Karte zu verdeutlichen, fehlen noch die Geo-Koordinaten der Bundesländer. Diese werden wie zuvor abgerufen.
geolocator = Nominatim(user_agent="my_app")
result={}
for entry in df_grp_states['state']:
result[entry] = geolocator.geocode(entry)res_dict={}
# extract lat/lon
for index, entry in result.items():
if entry:
res_dict[index] = { 'latitude': entry[1][0], 'longitude': entry[1][1] }
geocode_result = pd.DataFrame().from_dict(res_dict).T
geocode_result['state'] = geocode_result.index
geocode_result| latitude | longitude | state | |
|---|---|---|---|
| Baden-Württemberg | 48.537750 | 9.041169 | Baden-Württemberg |
| Bayern | 48.946756 | 11.403872 | Bayern |
| Berlin | 52.517037 | 13.388860 | Berlin |
| Brandenburg | 52.845549 | 13.246130 | Brandenburg |
| Bremen | 53.075820 | 8.807165 | Bremen |
| Hamburg | 53.550341 | 10.000654 | Hamburg |
| Hessen | 50.608065 | 9.028465 | Hessen |
| Mecklenburg-Vorpommern | 53.773506 | 12.575547 | Mecklenburg-Vorpommern |
| Niedersachsen | 52.839853 | 9.075962 | Niedersachsen |
| Nordrhein-Westfalen | 51.478921 | 7.554375 | Nordrhein-Westfalen |
| Rheinland-Pfalz | 49.953160 | 7.310646 | Rheinland-Pfalz |
| Saarland | 49.384187 | 6.953737 | Saarland |
| Sachsen | 50.929580 | 13.458505 | Sachsen |
| Sachsen-Anhalt | 52.008907 | 11.700334 | Sachsen-Anhalt |
| Schleswig-Holstein | 54.185400 | 9.822009 | Schleswig-Holstein |
| Thüringen | 50.901472 | 11.037784 | Thüringen |
| Deutschland | 51.163818 | 10.447831 | Deutschland |
df_grp_states_geo = pd.merge(df_grp_states, geocode_result, how='left', on='state')
df_grp_states_geo.drop(16, inplace=True) # drop germanyIn diesem Fall wird plotly genutzt, um eine Karte anzuzeigen.
Die Größe der Punkte zeigt die Anzahl der Haltestationen an.
Die Farbe gibt das Verhältnis zur Fläche an: Grün steht für eine hohe Dichte an Stationen, Rot für eine niedrige.
Hier wird nochmal deutlich, dass die Stadtstaaten eine hohe Dichte an Haltestationen aufweisen, allerdings absolut gesehen wenige Stationen haben (kleiner Kreis).
Die großen Bundesländer sind eher im mittleren Farbschema, wohingegen die Bundesländer im Norden rot gefärbt und damit das Schlusslicht bilden.
fig = px.scatter_geo(df_grp_states_geo,
lat='latitude',
lon='longitude',
hover_name='state', # Data to display when hovering over each data point
size='count', # Size of the markers
color='ratio', # Color of the markers
color_continuous_scale=['green','orange','red'],
projection='mercator',
scope='europe',
width=650,
height=800) # Map projection
fig.update_geos(center=dict(lon=10, lat=51), projection_scale=10)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
d) Verkehrsmittel und Verkehrsverbünde
Da die Verkehrsmittel und Verkehrsverbünde in geschachtelten Listen vorliegen, müssen diese zunächst geebnet werden, um die absolute Anzahl herauszufinden.
transports = []
for entry in df['transportAssociations']:
try:
for e in entry:
transports.append(e)
except:
pass
transportAssociations = pd.Series(transports).value_counts()transports = []
for entry in df['availableTransports']:
try:
for e in entry:
transports.append(e)
except:
pass
availableTransports = pd.Series(transports).value_counts()def plotBar(data, title, xlabel, ylabel, showlegend):
fig = px.bar(data, title=title)
fig.update_xaxes(title_text=xlabel)
fig.update_yaxes(title_text=ylabel)
fig.update_traces(showlegend=showlegend)
return figfig = plotBar(transportAssociations, 'Available Transport Associations', 'Transport Associations', 'Count', False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
transportAssociations.count()49
Spitzenreiter ist auch hier wieder Berlin, gefolgt von dem Rhein-Main-Verkehrsverbund (RMV).
Der RMV operiert in Hessen und ist der Nachfolger des Frankfurter Verkehrsverbundes (FVV) (Quelle: Wikipedia) Auf dem nachfolgenden Bild sieht man das Verkehrsgebiet in Hessen.
Die “NASA” ist der Nahverkehrsservice der Sachsen-Anhalt GmbH und hat lustigerweise auch die Webseite https://www.nasa.de/.
Auf dem Plan ist zu sehen, dass hauptsächlich die Städte Leipzig, Halle, Dessau und Magdeburg verbunden werden. Der Verkehrsverbund “NASA” besteht wiederum aus anderen, die ebenfalls hier auftauchen, wie der MDV. Es gibt Übergänge zu anderen Verkehrsverbünden wie VMT, VRB und VBB.
Die in unserer Region bekannteren Verkehrsverbünde VVS und NALDO rangieren auf den mittleren Plätzen. Insgesamt gibt es 49 unterschiedliche Verkehrsverbünde.
fig = plotBar(availableTransports, 'Available Transports', 'Transport Type', 'Count', False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Mit großem Abstand halten an den meisten Haltestationen die REGIONAL_TRAIN. Leider ist nicht dokumentiert, was unter “Regional Train” zu verstehen ist.
Nach manueller Untersuchung der Daten, sind damit sowohl Regionalbahnen (RB) wie auch S-Bahnen gemeint. Allerdings werden die S-Bahn Stationen in der Stadt mit CITY_TRAIN markiert. Das ist vorliegend also nicht ganz eindeutig. CITY_TRAIN sind die Stadtbahnen/Tram/U-Bahn sowie S-Bahnen in der Stadt.
Neben Zügen werden hier auch Busse (BUS) erfasst.
Im Vergleich dazu kommen die Schnellzüge INTERCITY_TRAIN (IC), HIGH_SPEED_TRAIN (ICE) und INTER_REGIONAL_TRAIN (IRE) fast schon selten vor. Das macht aber natürlich Sinn, weil diese nur an ausgewählten Bahnhöfen halten.
Hier gibt es eine Übersicht der Bahn über die Nah- und Fernverkehrszüge.
e) Bahnhofskategorien
Laut Theorie klassifiziert die Preisklasse (bis 2018 Bahnhofskategorie) anhand verschiedener Faktoren die Bedeutung eines Bahnhofs für den Personenverkehr sowie den Service, der dort geboten wird. (Quelle: Wikipedia)
Demnach wird die Preisklasse aufgrund folgender Kriterien ermittelt:
Daraus lässt sich ein Wert errechnen, der die Preisklasse angibt:
Beispiel für einen Bahnhof mit 4 Bahnsteigkanten von max. 250 Meter Länge, 20.000 Reisenden und 280 Zughalten am Tag, ohne anwesenden Mitarbeiter, jedoch mit technischer Stufenfreiheit:
Der Bahnhof in dem Beispiel hätte also Klasse 3.
Laut Wikipedia ergibt sich für Deutschland folgende Einteilung. Es wird nun geprüft, ob sich dies mit den vorhandenen Daten deckt.
| Kategorie | Anzahl | Beispiel |
|---|---|---|
| 1 | 21 | Berlin, Hamburg |
| 2 | 87 | Mainz, Trier |
| 3 | 256 | Hof, Bitterfel |
| 4 | 628 | Meiningen, Bingen |
| 5 | 992 | Köln-Holweide, Hohen Neuendorf |
| 6 | 2505 | Ilmenau, Glöwen |
| 7 | 916 | Zwotental, Göhrde |
Aufgrund des merge kommen nun ein paar Bahnhöfe doppelt vor, in denen es zwei Haltestationen gibt. Meist sind das größere Bahnhöfe, in denen die Schnellzüge getrennt vom Regionalverkehr abfahren, wie beispielsweise in Berlin.
Um das zu korrigieren, wird jeweils nur der letzte Eintrag bei diesen doppelten Einträgen verwendet.
df[df['id'] == 1071]| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 854 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [CITY_TRAIN] | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 855 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 856 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
unique_df = df.drop_duplicates(subset='id', keep='last')Betrachtet man nun die Daten von Deutschland, ergibt sich folgendes Bild:
unique_df.groupby(by='stationCategory')['id'].count()stationCategory
CATEGORY_1 23
CATEGORY_2 84
CATEGORY_3 275
CATEGORY_4 641
CATEGORY_5 977
CATEGORY_6 2774
CATEGORY_7 904
Name: id, dtype: int64
Im Ergebnis decken sich die Daten mit der Einteilung aus Wikipedia.
In Berlin gibt es gleich mehrere Bahnhöfe mit Preisklasse / stationCategory 1. In Hamburg ist es der Hauptbahnhof und Altona. Stuttgart Hbf gehört ebenfalls zur Kategorie 1.
unique_df[unique_df['stationCategory'] == 'CATEGORY_1']| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 183 | 220 | Augsburg | DE | 1 | 48.365441 | 10.885570 | {} | Augsburg Hbf | RB Süd | DB S&S | 86150 | Bayern | CATEGORY_1 | Viktoriastr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AVV] | https://api.railway-stations.org/photos/de/220... |
| 421 | 528 | Berlin | DE | 1-3 | 52.548963 | 13.388513 | {} | Berlin Gesundbrunnen | RB Ost | DB S&S | 13357 | Berlin | CATEGORY_1 | Badstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VBB] | https://api.railway-stations.org/photos/de/528... |
| 424 | 530 | Berlin | DE | 3 | 52.510488 | 13.434681 | {} | Berlin Ostbahnhof | RB Ost | DB S&S | 10243 | Berlin | CATEGORY_1 | Koppenstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/530... |
| 856 | 1071 | Berlin | DE | 1 | 52.525592 | 13.369545 | {} | Berlin Hauptbahnhof | RB Ost | DB S&S | 10557 | Berlin | CATEGORY_1 | Europaplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VBB] | https://api.railway-stations.org/photos/de/107... |
| 1002 | 1289 | Dortmund | DE | 15 | 51.517896 | 7.459290 | {} | Dortmund Hbf | RB West | DB S&S | 44137 | Nordrhein-Westfalen | CATEGORY_1 | Königswall | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR, WT] | https://api.railway-stations.org/photos/de/128... |
| 1052 | 1343 | Dresden | DE | 4 | 51.040563 | 13.732035 | {} | Dresden Hbf | RB Südost | DB S&S | 01069 | Sachsen | CATEGORY_1 | Wiener Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VVO] | https://api.railway-stations.org/photos/de/134... |
| 1075 | 1374 | Duisburg | DE | 1 | 51.429785 | 6.775903 | {} | Duisburg Hbf | RB West | DB S&S | 47051 | Nordrhein-Westfalen | CATEGORY_1 | Portsmouthplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR] | https://api.railway-stations.org/photos/de/137... |
| 1097 | 1401 | Düsseldorf | DE | 14 | 51.219962 | 6.794319 | {} | Düsseldorf Hbf | RB West | DB S&S | 40210 | Nordrhein-Westfalen | CATEGORY_1 | Konrad-Adenauer-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRS, VRR] | https://api.railway-stations.org/photos/de/140... |
| 1329 | 1690 | Essen | DE | 5 | 51.451355 | 7.014793 | {} | Essen Hbf | RB West | DB S&S | 45127 | Nordrhein-Westfalen | CATEGORY_1 | Am Hauptbahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRR] | https://api.railway-stations.org/photos/de/169... |
| 1476 | 1866 | Frankfurt am Main | DE | NaN | 50.107145 | 8.663789 | {} | Frankfurt (Main) Hbf | RB Mitte | DB S&S | 60329 | Hessen | CATEGORY_1 | Im Hauptbahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [RMV] | https://api.railway-stations.org/photos/de/186... |
| 1947 | 2514 | Hamburg | DE | 16 | 53.552736 | 10.006909 | {} | Hamburg Hbf | RB Nord | DB S&S | 20099 | Hamburg | CATEGORY_1 | Hachmannplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, SH, HVV] | https://api.railway-stations.org/photos/de/251... |
| 1951 | 2517 | Hamburg | DE | 17 | 53.552695 | 9.935175 | {} | Hamburg-Altona | RB Nord | DB S&S | 22765 | Hamburg | CATEGORY_1 | Scheel-Plessen-Str. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, SH, HVV] | https://api.railway-stations.org/photos/de/251... |
| 1977 | 2545 | Hannover | DE | 1 | 52.376761 | 9.741021 | {} | Hannover Hbf | RB Nord | DB S&S | 30159 | Niedersachsen | CATEGORY_1 | Ernst-August-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [IAM, GVH] | https://api.railway-stations.org/photos/de/254... |
| 2405 | 3107 | Karlsruhe | DE | 1a | 48.993515 | 8.402181 | {} | Karlsruhe Hbf | RB Südwest | DB S&S | 76137 | Baden-Württemberg | CATEGORY_1 | Bahnhofplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [KVV] | https://api.railway-stations.org/photos/de/310... |
| 2554 | 3320 | Köln | DE | 11 | 50.943030 | 6.958729 | {} | Köln Hbf | RB West | DB S&S | 50667 | Nordrhein-Westfalen | CATEGORY_1 | Trankgasse | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRS] | https://api.railway-stations.org/photos/de/332... |
| 2564 | 3329 | Köln | DE | 7 | 50.940874 | 6.975001 | {} | Köln Messe/Deutz | RB West | DB S&S | 50679 | Nordrhein-Westfalen | CATEGORY_1 | Ottoplatz | [REGIONAL_TRAIN, BUS, HIGH_SPEED_TRAIN, CITY_T... | [VRS] | https://api.railway-stations.org/photos/de/332... |
| 2784 | 3631 | Leipzig | DE | 5 | 51.345471 | 12.382064 | {} | Leipzig Hbf | RB Südost | DB S&S | 04109 | Sachsen | CATEGORY_1 | Willy-Brandt-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [NASA, MDV] | https://api.railway-stations.org/photos/de/363... |
| 3003 | 3925 | Mannheim | DE | 17 | 49.479354 | 8.468921 | {} | Mannheim Hbf | RB Südwest | DB S&S | 68161 | Baden-Württemberg | CATEGORY_1 | Willy-Brandt-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VRN] | https://api.railway-stations.org/photos/de/392... |
| 3236 | 4234 | München | DE | 10a | 48.140232 | 11.558335 | {} | München Hbf | RB Süd | DB S&S | 80335 | Bayern | CATEGORY_1 | Bayerstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [MVV] | https://api.railway-stations.org/photos/de/423... |
| 3243 | 4241 | München | DE | 11 | 48.127440 | 11.604971 | {} | München Ost | RB Süd | DB S&S | 81667 | Bayern | CATEGORY_1 | Orleansplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [MVV] | https://api.railway-stations.org/photos/de/424... |
| 3531 | 4593 | Nürnberg | DE | 9 | 49.445616 | 11.082989 | {} | Nürnberg Hbf | RB Süd | DB S&S | 90443 | Bayern | CATEGORY_1 | Bahnhofsplatz | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [VGN] | https://api.railway-stations.org/photos/de/459... |
| 3743 | 4859 | Berlin | DE | 1 | 52.475047 | 13.365319 | {} | Berlin Südkreuz | RB Ost | DB S&S | 12101 | Berlin | CATEGORY_1 | General-Pape-Straße | [] | [VBB] | https://api.railway-stations.org/photos/de/485... |
| 4622 | 6071 | Stuttgart | DE | 2 | 48.784084 | 9.181635 | {} | Stuttgart Hbf | RB Südwest | DB S&S | 70173 | Baden-Württemberg | CATEGORY_1 | Arnulf-Klett-Platz | [INTERCITY_TRAIN, REGIONAL_TRAIN, HIGH_SPEED_T... | [VVS] | https://api.railway-stations.org/photos/de/607... |
Betrachtet man die Bundesländer nach Kategorie, zeigt sich, dass einige Bundesländer keinen einzigen Bahnhof mit Preisklasse 1 besitzen.
df_state_categories = pd.DataFrame(unique_df.groupby(by=['state', 'stationCategory'])['id'].count())
df_state_categories = df_state_categories.reset_index().pivot(index='state', columns='stationCategory', values='id')
df_state_categories| stationCategory | CATEGORY_1 | CATEGORY_2 | CATEGORY_3 | CATEGORY_4 | CATEGORY_5 | CATEGORY_6 | CATEGORY_7 |
|---|---|---|---|---|---|---|---|
| state | |||||||
| Baden-Württemberg | 3.0 | 10.0 | 51.0 | 86.0 | 133.0 | 328.0 | 109.0 |
| Bayern | 4.0 | 9.0 | 48.0 | 100.0 | 174.0 | 532.0 | 158.0 |
| Berlin | 4.0 | 7.0 | 12.0 | 72.0 | 34.0 | 4.0 | NaN |
| Brandenburg | NaN | 4.0 | 13.0 | 22.0 | 44.0 | 174.0 | 53.0 |
| Bremen | NaN | 1.0 | 1.0 | 3.0 | 8.0 | 3.0 | NaN |
| Hamburg | 2.0 | 2.0 | 8.0 | 35.0 | 9.0 | 2.0 | NaN |
| Hessen | 1.0 | 8.0 | 24.0 | 62.0 | 109.0 | 248.0 | 27.0 |
| Mecklenburg-Vorpommern | NaN | 1.0 | 7.0 | 12.0 | 14.0 | 90.0 | 56.0 |
| Niedersachsen | 1.0 | 8.0 | 15.0 | 51.0 | 68.0 | 165.0 | 49.0 |
| Nordrhein-Westfalen | 6.0 | 16.0 | 42.0 | 101.0 | 166.0 | 307.0 | 73.0 |
| Rheinland-Pfalz | NaN | 7.0 | 12.0 | 37.0 | 67.0 | 222.0 | 74.0 |
| Saarland | NaN | 1.0 | 6.0 | 3.0 | 14.0 | 47.0 | 6.0 |
| Sachsen | 2.0 | 2.0 | 7.0 | 26.0 | 68.0 | 291.0 | 82.0 |
| Sachsen-Anhalt | NaN | 2.0 | 9.0 | 11.0 | 26.0 | 162.0 | 79.0 |
| Schleswig-Holstein | NaN | 4.0 | 10.0 | 10.0 | 28.0 | 49.0 | 36.0 |
| Thüringen | NaN | 2.0 | 10.0 | 10.0 | 15.0 | 150.0 | 102.0 |
Die größten Unterschiede gibt es in Preisklasse 6, hier liegt die Anzahl der Bahnhöfe je Bundesland weit auseinander. Bayern und Baden-Württemberg, welche über die meisten Bahnhöfe verfügen, sind Ausreißer.
In den restlichen Preisklassen ist das nicht so ausgeprägt, besonders NRW (gelb) ist in den oberen Kategorien zusammen mit Bayern und Baden-Württemberg häufig auf den besten Plätzen mit dabei.
fig = px.violin(df_state_categories, y=df_state_categories.columns,color=df_state_categories.index)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
In diesem Bild sieht man nochmal gut, dass die meisten Stationen in Preisklasse 6 sind. Bei den Stadtstaaten sind die Werte näher beieinander, dort ist die Anzahl der Stationen insgesamt natürlich geringer.
fig = px.scatter(df_state_categories, y=df_state_categories.columns)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Rolltreppen und Aufzüge
df_facilities.head(1)| id | description | operatorname | state | stateExplanation | type | |
|---|---|---|---|---|---|---|
| 0 | 1 | zu Gleis 1 | DB Station&Service | ACTIVE | available | ELEVATOR |
Zunächst werden die Daten in eine Form gebracht, die besser zu visualisieren ist.
Von Interesse ist, welche Art (type) von facilities sich in welchem Zustand (state) befinden.
Dazu wird anhand dieser beiden Werte gruppiert und die summierten Werte wieder in eine flache Struktur geformt.
Man könnte auch noch auf den operatorname eingehen, allerdings werden die allermeisten Einrichtungen wieder von der DB Station&Service betrieben.
df_facilities_grouped = df_facilities.groupby(['type', 'state']).count()
df_facilities_grouped = df_facilities_grouped.unstack()['id']
df_facilities_grouped| state | ACTIVE | INACTIVE | UNKNOWN |
|---|---|---|---|
| type | |||
| ELEVATOR | 2392 | 126 | 51 |
| ESCALATOR | 820 | 140 | 21 |
df_facilities_grouped['Ratio'] = (df_facilities_grouped['INACTIVE']) / df_facilities_grouped['ACTIVE']
df_facilities_grouped| state | ACTIVE | INACTIVE | UNKNOWN | Ratio |
|---|---|---|---|---|
| type | ||||
| ELEVATOR | 2392 | 126 | 51 | 0.052676 |
| ESCALATOR | 820 | 140 | 21 | 0.170732 |
Es gibt ein paar Daten, bei denen der Zustand unbekannt ist.
Darüber hinaus sind absolut und relativ gesehen aktuell mehr Rolltreppen als Aufzüge kaputt.
Relativ sind es mit aktuell über 15% (nach Update eine Woche später: 17%) nicht funktionierende Rolltreppen sehr viele.
fig = go.Figure()
for state in df_facilities_grouped.columns[:3]:
fig.add_trace(go.Bar(
x=df_facilities_grouped.index,
y=df_facilities_grouped[state],
name=state,
))
fig.update_layout(title='Zustand der Einrichtungen an Bahnhöfen',
xaxis_title='Typ',
yaxis_title='Anzahl',
barmode='group')
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Dashboard für Haltestationen
Wir können uns eine Karte anzeigen lassen, die alle Haltestationen mit zusätzliche Informationen anzeigt.
Ganz Deutschland anzuzeigen führt allerding zu Performanceproblemen, daher werden zunächst alle Marker ausgeblendet, welche über den Filter je Bundesland wieder hinzugeschaltet werden können.
df.dropna(subset = ['latitude'], inplace=True)Dazu wird zuerst pro Bundesland eine FeatureGroup erstellt, die initial ausgeblendet ist.
state_dict = {}
for i in df.index:
state_dict.setdefault(df['state'][i], folium.FeatureGroup(name=df['state'][i], show=False, autoZIndex=False))
state_dict{'Nordrhein-Westfalen': <folium.map.FeatureGroup at 0x2303a3b6ce0>,
'Baden-Württemberg': <folium.map.FeatureGroup at 0x2303a3b6c80>,
'Bayern': <folium.map.FeatureGroup at 0x2303a3b62c0>,
'Niedersachsen': <folium.map.FeatureGroup at 0x230389aba00>,
'Sachsen': <folium.map.FeatureGroup at 0x230389ab940>,
'Schleswig-Holstein': <folium.map.FeatureGroup at 0x23039d75f30>,
'Berlin': <folium.map.FeatureGroup at 0x23037be0c10>,
'Brandenburg': <folium.map.FeatureGroup at 0x23039ab26e0>,
'Rheinland-Pfalz': <folium.map.FeatureGroup at 0x23039ab2890>,
'Hessen': <folium.map.FeatureGroup at 0x23039ab1f60>,
'Hamburg': <folium.map.FeatureGroup at 0x2303a509570>,
'Mecklenburg-Vorpommern': <folium.map.FeatureGroup at 0x2303a50a050>,
'Thüringen': <folium.map.FeatureGroup at 0x2303a509720>,
'Sachsen-Anhalt': <folium.map.FeatureGroup at 0x2303a509ab0>,
'Saarland': <folium.map.FeatureGroup at 0x2303a5095a0>,
'Schweiz CH': <folium.map.FeatureGroup at 0x2303a5091b0>,
'Bremen': <folium.map.FeatureGroup at 0x2303a508160>}
Per HTML kann ein Popup definiert werden, welches erscheint, sobald man auf den Pin klickt. Hierbei werden das Bild und die zugehörigen Verkehrsverbünde sowie Zugtypen angezeigt.
Auf dieser Karte kann man sehr schön erkennen, wo die Bahnlinien verlaufen und welche Regionen aktuell nicht an die Bahn angeschlossen sind.
Beim Klicken durch die Bilder fällt auch auf, dass die Bahnhöfe häufig in älteren Gebäuden untergebracht sind, von den allerdings viele renoviert wurden.
Die roten Marker identifizieren Bahnhöfe, in denen ICEs halten. In den gelben Markern halten “nur noch” die IREs.
def GetIcon(availableTransports):
try:
if ('HIGH_SPEED_TRAIN' in availableTransports):
return folium.Icon(color='red', icon='map-marker')
elif ('INTERCITY_TRAIN' in availableTransports):
return folium.Icon(color='orange', icon='map-marker')
except:
return folium.Icon(color='blue', icon='map-marker')map_df = df
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
# this is probably done too often, but folium is smart enough
feature_group = state_dict[map_df['state'][i]]
m.add_child(feature_group)
folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
).add_to(feature_group)
folium.LayerControl(collapsed=False).add_to(m)
mBahnhofsnahe Dienstleistungen
Mögliche Werte von Type aus der Dokumentation:
- INFORMATION_COUNTER [Informationsstand für Belange im Bahnhof (kein Fahrkartenverkauf)]
- TRAVEL_CENTER [Reisezentrum]
- VIDEO_TRAVEL_CENTER [Video Reisezentrum]
- TRIPLE_S_CENTER [3S Zentrale für Service, Sicherheit & Sauberkeit]
- TRAVEL_LOUNGE [Lounge (DB Lounge z.B.)]
- LOST_PROPERTY_OFFICE [Fundstelle]
- RAILWAY_MISSION [Bahnhofsmission]
- HANDICAPPED_TRAVELLER_SERVICE [Service für mobilitätseingeschränkte Reisende]
- LOCKER [Schließfächer]
- WIFI [WLan]
- CAR_PARKING [Autoparkplatz, ggf. kostenpflichtig]
- BICYCLE_PARKING [Fahrradparkplätze, ggf. kostenpflichtig]
- PUBLIC_RESTROOM [Öffentliches WC, ggf. kostenpflichtig]
- TRAVEL_NECESSITIES [Geschäft für den Reisendenbedarf]
- CAR_RENTAL [Car-Sharer oder Mietwagen]
- BICYCLE_RENTAL [Mieträder]
- TAXI_RANK [Taxi Stand]
- MOBILE_TRAVEL_SERVICE [Mobiler Service]
- RAD_PLUS (Rad+ Gebiet)
df_local_services = loadData('local_services.pkl')df_local_services.head()| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | None | None | Mo-Su 06:15-22:30;PH 06:15-22:30 | MOBILE_TRAVEL_SERVICE | ||
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 2 | 1 | None | None | None | RAILWAY_MISSION | ||
| 3 | 1 | None | Ja, um Voranmeldung unter 030 65 21 28 88 (Ort... | None | HANDICAPPED_TRAVELLER_SERVICE | ||
| 4 | 1 | None | None | None | LOCKER |
Man erkennt, dass die TRIPLE_S_CENTER (Service, Sicherheit & Sauberkeit), TRAVEL_CENTER, TRAVEL_LOUNGE und VIDEO_TRAVEL_CENTER alle einen Namen, Öffnungszeiten und Geo-Koordinaten haben.
Die Services RAD_PLUS und TRAVEL_LOUNGE haben einen Namen, MOBILE_TRAVEL_SERVICE, INFORMATION_COUNTER und LOST_PROPERTY_OFFICE dafür Öffnungszeiten.
df_local_services.groupby(by='type').count().sort_values(by='id', ascending=False)| id | name | description | openingHours | latitude | longitude | |
|---|---|---|---|---|---|---|
| type | ||||||
| TRIPLE_S_CENTER | 4103 | 4103 | 0 | 4103 | 4103 | 4103 |
| CAR_PARKING | 3136 | 0 | 0 | 0 | 3136 | 3136 |
| BICYCLE_PARKING | 3081 | 0 | 0 | 0 | 3081 | 3081 |
| TAXI_RANK | 1100 | 0 | 0 | 0 | 1100 | 1100 |
| PUBLIC_RESTROOM | 562 | 0 | 0 | 0 | 562 | 562 |
| TRAVEL_NECESSITIES | 508 | 0 | 0 | 0 | 508 | 508 |
| HANDICAPPED_TRAVELLER_SERVICE | 318 | 0 | 233 | 0 | 318 | 318 |
| TRAVEL_CENTER | 260 | 260 | 260 | 260 | 260 | 260 |
| RAD_PLUS | 260 | 260 | 0 | 0 | 260 | 260 |
| LOCKER | 168 | 0 | 0 | 0 | 168 | 168 |
| MOBILE_TRAVEL_SERVICE | 127 | 0 | 0 | 127 | 127 | 127 |
| WIFI | 119 | 0 | 0 | 0 | 119 | 119 |
| RAILWAY_MISSION | 89 | 0 | 0 | 0 | 89 | 89 |
| VIDEO_TRAVEL_CENTER | 87 | 87 | 87 | 87 | 87 | 87 |
| INFORMATION_COUNTER | 76 | 0 | 0 | 76 | 76 | 76 |
| LOST_PROPERTY_OFFICE | 70 | 0 | 70 | 70 | 70 | 70 |
| CAR_RENTAL | 67 | 0 | 0 | 0 | 67 | 67 |
| TRAVEL_LOUNGE | 14 | 14 | 0 | 14 | 14 | 14 |
def getDataByType(type):
filtered_ids = df_local_services[df_local_services['type'] == type]['id']
count = filtered_ids.count()
unique_count = filtered_ids.nunique()
print(type,"Count:", count)
print(type,"unique Count:", unique_count)
return df_local_services[df_local_services['type'] == type]df_triples_center = getDataByType('TRIPLE_S_CENTER')TRIPLE_S_CENTER Count: 4103
TRIPLE_S_CENTER unique Count: 4103
Wenn man sich die Daten anschaut, tauchen dort viele IDs doppelt auf und es scheint, als wären die Daten schwierig zu interpretieren.
Gruppiert man die Daten allerdings anhand des Typs, sind die IDs eindeutig und können somit gut analysiert werden.
df_triples_center.head(3)| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 1 | 1 | Duisburg Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 13 | 1000 | Dresden | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER | ||
| 16 | 1002 | Frankfurt (Main) Hbf | None | Mo-Su 00:00-24:00;PH 00:00-24:00 | TRIPLE_S_CENTER |
Die Öffnungszeiten sind in einem bestimmten Format angegeben, daher müssen diese erst geparst werden.
Zuvor wird geprüft, ob es überhaupt Abweichungen gibt.
df_triples_center['openingHours'].unique()array(['Mo-Su 00:00-24:00;PH 00:00-24:00'], dtype=object)
Scheinbar haben alle TRIPLE_S_CENTER durchgehend geöffnet.
df_travel_center = getDataByType('TRAVEL_CENTER')TRAVEL_CENTER Count: 260
TRAVEL_CENTER unique Count: 257
df_travel_center.head(1)| id | name | description | openingHours | latitude | longitude | type | |
|---|---|---|---|---|---|---|---|
| 11 | 1 | DB Reisezentrum Aachen Hbf | Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00 | 50.768944 | 6.0902 | TRAVEL_CENTER |
Bei den Reisezentren sehen die Öffnungszeiten schon interessanter aus.
df_travel_center['openingHours'].unique()array(['Mo-Fr 06:00-21:00;Sa 07:00-20:00;Su 08:00-20:00',
'Mo-We 08:00-12:30,13:00-17:00;Th-Fr 08:00-12:30,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00;Su 10:00-15:00',
'Mo 06:00-11:00,12:00-16:00;Tu-We,Fr 08:00-13:00,14:00-16:00;Th 09:00-13:00,14:00-19:00;Sa 08:00-12:00',
'Mo-Fr 06:30-12:00,13:00-18:30;Sa 08:00-13:00',
'Mo-Su 07:00-21:00',
'Mo-Fr 07:30-18:30;Sa-Su 09:00-13:00,13:30-17:00',
'Mo-Fr 09:00-12:00,13:00-17:25',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:30-13:30',
'Mo-Fr 08:00-18:00', 'Mo-Fr 07:00-19:00;Sa 09:00-14:00',
'Mo-Fr 07:00-11:30,12:00-14:30;Sa 08:30-13:30',
'Mo-Fr 06:30-09:00,09:30-17:30',
'Mo 06:30-18:30;Tu-Fr 07:30-18:30;Sa 06:30-11:30,12:00-14:30;Su 10:30-14:30,15:00-18:30',
'Mo-Fr 06:30-12:00,13:00-18:30', 'Mo-Fr 09:00-12:30,13:30-17:00',
'Mo 06:00-10:30,11:30-16:00;Tu 09:30-13:30,14:30-19:00;We-Fr 08:30-12:30,13:30-16:00;Sa 08:00-12:00',
'Mo-Fr 09:00-12:30,14:00-17:45',
'Mo 06:00-11:30,12:20-16:30;Tu-Fr 07:00-11:30,12:15-16:35',
'Mo-Fr 07:30-19:00;Sa-Su 09:00-18:00',
'Mo,Fr 06:15-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Tu-Th 06:15-12:00,12:45-16:35 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. ";Sa 07:10-12:00 open "Am 01.08.23 ist das Reisezentrum ;krankheitsbedingt geschlossen. ;Wir bitten um Ihr Verständnis. "',
'Mo-Fr 06:45-20:00;Sa-Su 07:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-16:00', 'Mo-Fr 06:30-11:45',
'Mo-Fr 07:00-18:00;Sa 08:00-12:00,12:30-14:30',
'Mo-Fr 07:00-21:00;Sa-Su 09:00-21:00',
'Mo 06:00-15:00,15:45-20:00;Tu-Fr 07:00-12:45,13:45-17:00;Sa-Su 09:30-15:00',
'Mo-We,Fr 08:30-13:00,13:45-18:00', 'Mo-Fr 06:30-17:30',
'Mo-Fr 05:45-18:45;Sa 08:00-13:00',
'Mo-Fr 07:30-12:30,13:30-17:30;Sa 07:30-12:30',
'Mo-Fr 08:00-13:00,14:00-17:45',
'Mo-Fr 07:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 08:30-18:30;Sa-Su 09:30-16:30',
'Mo 07:00-12:15,13:00-16:30;Tu-We,Fr 09:00-12:15,13:00-16:30;Th 09:00-12:15,13:00-18:00',
'Mo-Fr 07:30-19:00;Sa 08:15-15:00;Su 09:15-15:00',
'Mo-Fr 07:30-18:30;Sa 09:00-14:00',
'Mo 06:00-17:30;Tu-We,Fr 09:00-17:30;Th 09:00-20:00;Sa 08:00-16:30;Su 09:00-12:30,13:00-16:30',
'Mo-Fr 06:00-20:00;Sa 07:30-12:15,12:45-15:30',
'Mo-Fr 07:30-21:00;Sa-Su 08:30-18:30',
'Mo 06:30-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";Tu,Fr 07:00-17:50 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!";We-Th 07:00-11:00,11:45-16:15 open "Aufgrund von kurzfristiger Krankheit ist;das Rz Forchheim am Dienstag, 01.08.2023;bis 15:30 Uhr geöffnet!"',
'Mo-Fr 07:00-19:00;Sa 07:30-11:30,12:00-16:00;Su 09:30-15:00',
'Mo-Fr 06:00-20:30;Sa 07:00-14:30',
'Mo-Fr 07:30-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:30-15:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 11:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-11:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:00-20:00;Sa 08:30-19:00;Su 08:30-20:00',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 09:00-13:45',
'Mo 07:15-12:00,13:00-17:45;Tu-Fr 08:00-12:00,13:00-17:45;Sa 08:00-13:30',
'Mo-Fr 07:00-18:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum.";Sa 08:00-13:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum."',
'Mo-Fr 08:00-12:00,13:00-18:00;Sa 08:00-13:30',
'Mo-Fr 07:00-18:00;Sa 09:00-14:00',
'Mo-Fr 08:00-18:30;Sa 08:15-16:30;Su 12:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-14:30', 'Mo-Fr 07:15-17:15',
'Mo-Fr 08:30-17:30;Sa 08:30-13:30',
'Mo-Fr 07:45-12:00,13:30-18:00;Sa 08:10-13:45',
'Mo-Fr 09:00-18:00',
'Mo,Fr 07:00-18:00;Tu-Th 08:00-12:30,13:15-18:00;Sa 08:00-13:15;Su 10:00-15:15',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:00-12:30,13:30-17:00;Sa 07:00-12:30,13:00-15:00;Su 09:30-15:00',
'Mo-Fr 07:00-18:30;Sa 09:00-14:00',
'Mo-Fr 08:00-19:00;Sa 09:00-18:00;Su 10:00-16:00',
'Mo-Fr 07:45-12:30,13:30-17:45;Sa 09:00-14:00',
'Mo,Fr 06:00-18:00;Tu-Th 06:45-12:00,12:45-17:00;Su 09:00-12:30,13:00-17:00',
'Mo-Fr 07:00-19:00;Sa 09:00-17:00;Su 10:00-17:00',
'Mo-Fr 07:00-12:15,13:00-17:00;Sa 09:00-12:30,13:00-17:00;Su 08:00-12:30,13:00-15:00',
'Mo-Fr 08:00-12:30,13:30-17:00;Sa 10:00-15:30',
'Mo-Fr 07:15-12:15,13:00-17:30;Sa-Su 08:10-12:15,12:45-15:50',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch persönlich im DB Videoreisezentrum"',
'Mo-Fr 06:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Sa 07:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr";Su 08:00-20:00 open "Veränderte Öffnungszeiten aus betrieblichen Gründen:;Mi, 07.06.23 von 06:00 - 18:00 Uhr;Sa, 10.06.23: 07:00-16:00/So, 11.06.23: 09:00-18:00 Uhr"',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:15', 'Mo-Fr 07:30-18:30',
'Mo-Fr 07:40-18:30;Sa 08:00-13:30;Su 09:00-14:30',
'Mo-Fr 06:30-19:00;Sa 08:00-17:30;Su 10:00-18:00',
'Mo-Fr 07:00-19:00;Sa 09:30-14:30',
'Mo-Sa 07:00-20:00;Su 08:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:15',
'Mo-Fr 07:45-13:00,13:45-17:30;Sa 07:45-12:45',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 07:45-12:30,13:30-18:00;Sa 08:45-14:20',
'Mo-Fr 07:30-18:30;Sa 08:30-15:00;Su 10:00-16:00',
'Mo 07:00-12:30,13:00-17:00;Tu,Fr 08:00-12:30,13:00-17:00;We-Th 08:00-12:30,13:00-18:00;Sa 07:30-12:30,13:00-15:00;Su 10:00-15:00',
'Mo,Th 06:30-18:00;Tu-We,Fr 06:30-12:00,13:00-17:00;Sa 06:30-12:00,13:00-15:00',
'Mo-Fr 06:50-18:00;Sa 08:30-13:00',
'Mo,Fr 06:45-18:00;Tu-Th 07:20-12:00,13:00-17:30',
'Mo-Th 08:00-12:00,12:45-16:30;Fr 08:00-13:30',
'Mo-We,Fr 07:30-11:30,12:30-17:30;Th 06:00-15:00,15:30-20:00;Sa 08:30-14:00;Su 10:00-15:00',
'Mo-Fr 09:00-13:00,14:00-17:30;Sa 09:00-13:00',
'Mo-Fr 07:45-12:00,12:45-17:30;Sa 08:00-13:00',
'Mo 06:00-20:00;Tu-Fr 07:00-20:00;Sa-Su 08:00-13:00,14:00-16:30',
'Mo-We,Fr 07:30-12:30,13:30-17:30;Th 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:45,13:45-18:00;Sa 08:30-13:00',
'Mo-Fr 08:00-12:00,13:00-16:30',
'Mo 06:00-15:00,15:30-20:00;Tu-Fr 07:30-12:30,13:30-17:30;Sa-Su 09:45-15:00',
'Mo-Fr 06:30-18:00;Sa 09:00-15:30;Su 11:00-17:00',
'Mo-Fr 08:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo,Th-Fr 08:00-16:30;Tu-We 08:00-12:30,13:00-17:00;Sa 08:00-12:30,13:00-16:00',
'Mo,Th-Fr 07:15-11:45,12:00-17:00;Tu-We 07:15-11:45,12:30-17:00;Su 10:00-15:00',
'Mo 12:30-18:00,8:00-12:00;Tu-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'Mo-Fr 07:00-19:00;Sa-Su 08:00-18:00',
'Mo-Fr 07:00-12:00,14:00-18:00;Sa 09:00-14:00',
'Mo-Fr 06:30-21:00;Sa 08:00-19:00;Su 09:00-20:00',
'Mo-Fr 08:00-12:30,13:30-18:00;Sa 09:00-13:00',
'Mo,We-Fr 07:00-12:45,13:45-17:00;Tu 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:00-12:00,12:45-17:00',
'Mo-Fr 07:00-19:30;Sa-Su 09:15-17:45',
'Mo-Fr 08:00-17:00;Sa 08:00-13:00',
'Mo-Fr 08:00-12:00,13:00-16:00',
'Mo-Fr 08:00-18:30;Sa 08:30-16:30',
'Mo-Fr 07:00-18:00;Sa 08:00-13:00',
'Mo-Fr 05:45-20:30;Sa-Su 06:45-20:00',
'Mo-Fr 08:00-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo 06:00-19:30;Tu-Fr 07:00-19:30;Sa 08:00-17:00;Su 09:00-17:30',
'Mo-Fr 06:00-21:00;Sa-Su 07:00-21:00',
'Mo 06:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Tu-Th 07:00-19:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Fr 07:00-20:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Sa 09:00-17:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 ";Su 10:00-18:00 open "08.06.2023 Fronleichnam 10:00 - 17:00 "',
'Mo 07:30-12:00,12:45-16:30;Tu-Fr 08:30-12:00,12:45-16:30',
'Mo-Fr 07:45-12:15,13:00-17:45',
'Mo-Fr 09:00-12:00,13:00-17:25 open "Bitte Beachten: ;Am Montag, 31.07.23 geschlossen "',
'Mo-Fr 06:50-12:30,13:15-17:05;Sa 07:30-11:00,11:30-15:30;Su 08:45-12:00,12:30-17:15',
'Mo-Fr 08:00-18:00;Sa 09:00-13:00',
'Mo-Fr 08:00-12:00,13:00-17:00',
'Mo-Fr 06:00-18:00;Sa 07:00-14:00;Su 08:30-16:00',
'Mo-Fr 08:00-12:00,12:45-17:45;Sa 08:00-13:00',
'Mo-Fr 08:45-12:00,12:45-17:00',
'Mo-Fr 08:00-20:00;Sa 10:00-20:00;Su 10:00-18:00',
'Mo-Fr 08:00-18:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo 07:00-11:30,12:15-16:00;Tu-Fr 08:00-12:15,13:00-16:00',
'Mo-Fr 08:30-12:15,12:45-16:00',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 10:00-19:00',
'Mo-Fr 07:00-12:30,13:30-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-18:15 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Sa 08:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr";Su 10:00-15:00 open "Bitte Beachten:;Am Montag, den 30.07.2023;07:30 - 11:45 und 12:30 - 16:00 Uhr"',
'Mo-Fr 08:30-13:00,14:00-18:00;Sa 08:30-13:00',
'Mo-Fr 05:45-20:00;Sa 06:30-19:30;Su 08:30-19:30',
'Mo-Fr 07:30-18:30 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum";Sa 09:00-14:00 open "Wir bedienen Sie auch gerne in unserem Videoreisezentrum"',
'Mo-Fr 08:00-11:30,12:00-17:00;Sa-Su 08:00-11:30,12:00-15:30',
'Mo-Fr 07:30-18:30;Sa 07:30-11:30,12:00-15:30;Su 09:30-13:30,14:00-17:30',
'Mo-Fr 06:00-20:00;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:15;Sa 07:00-19:00;Su 08:00-20:00',
'Mo-Fr 06:00-21:00;Sa-Su 08:00-20:30',
'Mo-Fr 08:00-19:00;Sa 08:00-16:00;Su 09:00-16:00',
'Mo-Fr 08:00-12:15,12:45-16:00',
'Mo-Fr 06:45-18:35;Sa 07:30-13:00',
'Mo 06:00-10:30,11:30-16:00;Tu,Th-Fr 08:30-12:30,13:30-16:00;We 09:30-12:30,13:30-19:00;Sa 08:00-12:00',
'Mo-Fr 08:00-12:00,12:30-18:00;Sa 08:00-12:00',
'24/7 closed "Dauerhaft geschlossen"', 'Mo-Fr 06:15-16:40',
'Mo-Fr 07:00-20:00;Sa-Su 09:00-18:00',
'Mo-Fr 06:50-20:00;Sa-Su 07:50-19:00',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 08:00-13:00,14:00-18:00',
'Mo-Fr 07:00-20:00;Sa-Su 08:00-18:00',
'Mo-Fr 08:00-12:00,13:00-16:30 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 06:00-10:30,11:00-15:30,16:00-21:00;Sa-Su 06:45-15:30,16:00-19:45',
'Mo-We 08:00-12:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Th-Fr 13:00-17:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 07:00-17:00;Sa 08:00-13:00',
'Mo-Fr 09:00-13:15,13:45-17:00',
'Mo-Fr 07:00-18:30;Sa 08:00-12:00,12:30-16:00;Su 10:00-15:30',
'Mo,We-Fr 08:00-12:30,13:30-18:00;Tu 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:30-12:20,12:50-16:30',
'Mo-We,Fr 07:00-12:45,13:45-17:00;Th 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 06:15-12:00,12:45-16:00;Sa 07:45-12:45',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-13:30',
'Mo-Fr 08:30-12:30,14:00-17:00',
'Mo,Fr 06:30-19:15 open "gültig ab 01.01.22";Tu-Th 07:30-12:30,13:15-17:15 open "gültig ab 01.01.22";Su 10:45-14:00,14:30-19:00 open "gültig ab 01.01.22"',
'Mo-Fr 07:00-21:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 09:00-19:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Su 10:00-20:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 09:00-12:30,13:00-16:45', 'Mo-Fr 09:10-12:15,13:30-17:50',
'Mo-Fr 07:00-20:00;Sa 08:00-18:00;Su 09:00-20:00',
'Mo-Fr 09:00-12:00,13:00-17:00 open "Nutzen Sie auch das Video-Reisezentrum im Vorraum"',
'Mo-Fr 09:00-12:30,13:00-18:00;Sa 09:00-13:00,13:30-16:00;Su 12:00-15:30,16:00-20:00',
'Mo-Fr 08:00-18:00;Sa 09:00-14:00',
'Mo-Fr 07:30-19:00;Sa 09:00-18:00;Su 10:00-18:30',
'Mo-Fr 06:30-18:00;Sa 08:00-17:00;Su 10:00-13:30',
'Mo-Fr 07:00-18:00;Sa-Su 10:00-15:15',
'Mo-Fr 08:30-12:00,12:45-16:15 open "Nutzen Sie auch das Video-Reisezentrum am Vorplatz"',
'Mo-Fr 07:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 09:00-13:00,13:30-17:00;Sa 08:00-13:00',
'Mo-Fr 06:15-12:00,12:35-16:50;Sa 08:30-14:00;Su 09:30-13:30',
'Mo-Th 08:00-18:00;Fr 08:00-13:00,14:00-18:00',
'Mo-Fr 10:00-16:00',
'Mo-Fr 08:00-12:30,13:15-18:00;Sa-Su 08:45-11:45,12:15-16:15',
'Mo-Fr 06:45-18:45;Sa 07:45-15:30',
'Mo-Fr 07:30-18:00;Sa 09:00-14:00;Su 09:30-13:30,14:00-17:00',
'Mo-Fr 08:15-12:15,13:00-17:45;Sa 07:15-12:30,13:00-15:45',
'Mo-Fr 07:30-18:30;Sa 08:30-14:00;Su 11:30-16:30',
'Mo 07:45-12:45,13:45-17:00;Tu,Th-Fr 07:00-12:45,13:45-17:00;We 06:00-15:00,15:45-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 07:30-13:00,14:00-18:00;Sa-Su 08:15-13:00,13:30-16:00',
'Mo-Fr 06:30-19:00;Sa 08:30-14:05',
'Mo-Fr 08:30-13:00,14:00-18:30;Sa 08:30-12:30',
'Mo-Fr 06:30-18:30;Sa 07:30-12:30;Su 11:30-16:30',
'Mo-Fr 08:00-20:00;Sa-Su 09:00-19:00',
'Mo-Fr 08:00-20:00;Sa-Su 10:00-20:00',
'Mo-Fr 07:00-18:00;Sa 08:30-16:00;Su 09:30-17:00',
'Mo-Fr 06:00-20:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 08:30-13:15,14:15-18:10;Sa 08:30-13:15',
'Mo-Th 07:15-12:00,12:30-15:45',
'Mo-Fr 06:00-21:15;Sa-Su 07:00-19:00',
'Mo-We,Fr 07:30-13:00,14:00-17:00;Th 06:00-15:00,15:30-20:00;Sa 07:30-11:00,11:30-15:00;Su 09:30-15:00',
'Mo-Fr 07:45-12:45,13:30-18:00;Sa-Su 08:30-12:30,13:00-15:15',
'Mo-Fr 08:00-18:00 open "Bitte beachten Sie unsere geänderten Öffnungszeit am ;21.07.23 von 08:00-12:30 und 13:00-16:00 Uhr; Wir bedienen Sie auch persönlich im DB Videoreisezentrum "',
'Mo-Fr 06:30-18:00 open "Wir danken für Ihr Verständnis";Sa 08:30-13:30 open "Wir danken für Ihr Verständnis"',
'Mo-Fr 08:30-11:30,12:30-16:55 open " Wir bedienen Sie auch persönlich im DB Videoreisezentrum ;am Bahnsteig 1. "',
'Mo-Fr 06:30-12:10,12:55-16:50;Sa 06:45-12:15',
'Mo-Fr 08:00-18:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten.";Sa 08:00-13:00 open "Das Digitale Ticket jetzt im DB Reisezentrum ;buchen und das Ticket bequem ;per E-Mail erhalten."',
'Mo-Fr 06:00-19:00;Sa 08:00-17:00;Su 09:00-18:00',
'Mo-Fr 07:00-19:00;Sa 08:30-17:45;Su 09:30-18:00',
'Mo-Fr 08:50-12:30,13:45-17:30',
'Mo-Tu,Th-Fr 07:30-11:50,12:40-15:40',
'Mo-Fr 07:00-19:00;Sa 08:00-16:00;Su 09:30-18:30',
'Mo-Fr 08:30-12:30,13:45-17:10',
'Mo-Fr 07:00-19:00;Sa 08:00-17:30;Su 10:00-15:30',
'Mo-Tu,Th-Fr 08:00-12:30,13:30-18:00;We 06:30-15:00,15:30-20:00;Sa-Su 09:30-15:00',
'Mo-Fr 08:15-17:30;Sa 09:15-13:00',
'Mo-Fr 08:00-12:30,13:30-17:30;Sa 08:30-12:30',
'Mo 06:00-17:30;Tu-Fr 07:30-17:30;Sa 07:00-09:45,10:15-13:30;Su 10:00-17:30',
'Mo-Fr 06:15-20:15;Sa-Su 08:15-18:15',
'Mo-Fr 06:00-22:00;Sa-Su 07:00-22:00',
'Mo-Fr 08:00-18:00;Sa 09:00-18:00',
'Mo-Fr 07:00-18:30;Sa 08:30-14:00',
'Mo 08:15-12:30,13:00-16:15;Tu-Fr 08:00-12:30,13:00-15:30;Sa 08:15-13:15'],
dtype=object)
def getDays(daysList):
days_of_week = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
start_index = days_of_week.index(daysList[0])
end_index = days_of_week.index(daysList[-1])
if start_index <= end_index:
days_between = days_of_week[start_index:end_index+1]
else:
days_between = days_of_week[start_index:] + days_of_week[:end_index+1]
return days_betweendef get_minute_intervals(interval, time_range):
start_str, end_str = time_range.split('-')
start_time = datetime.strptime(start_str, '%H:%M')
end_time = datetime.strptime(end_str, '%H:%M')
interval = timedelta(minutes=interval)
current_time = start_time
intervals = []
while current_time <= end_time:
intervals.append(current_time.strftime('%H:%M'))
current_time += interval
return intervalsAktuell beinhalten die Daten nur die Öffnungszeiten pro Tag, ohne die Angabe genauer Uhrzeiten. Dazu müsste vermutlich eine doppelte x-Achse verwenden, um sowohl die Tag- als auch die Uhrzeiten anzuzeigen.
data_rows = []
for entry in df_travel_center['openingHours']:
day_time_pairs = entry.split(';')
row = {}
for pair in day_time_pairs:
try:
days, times = pair.split(' ')
days = days.split('-')
fixed_list = []
for item in days:
if (',' in item):
day_item = item.split(',')
fixed_list.append(day_item)
else:
fixed_list.append(item)
allDays = getDays(fixed_list)
#print(allDays)
for day in allDays:
row[day] = 1
except Exception as e:
#print(e)
#times = pair.split(' ')
#print(times)
row[day] = 0
data_rows.append(row)
df_opening_hours = pd.DataFrame(data_rows)
df_opening_hours| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | NaN | NaN | 1.0 | NaN | 1.0 | NaN |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | NaN |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN |
260 rows × 7 columns
Man kann sehen, dass in der Tabelle häufig NaN auftaucht. Dass die Daten an dieser Stelle nicht verarbeitet werden konnten, lag daran, dass Kommentare in den Zeiten vorhanden waren, wie beispielsweise “Aufgrund von Krankheit geschlossen”.
Daher wird davon ausgegangen, dass NaN ebenfalls geschlossen bedeutet.
df_opening_hours.replace(np.nan, 0, inplace=True)
df_opening_hours| Mo | Tu | We | Th | Fr | Sa | Su | |
|---|---|---|---|---|---|---|---|
| 0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 1 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 2 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 3 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 |
| 4 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 255 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
| 256 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 257 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
| 258 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | 0.0 |
| 259 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
260 rows × 7 columns
Generell ist zu erkennen, dass die meisten Reisezentren am Wochenende, vor allem sonntags, geschlossen haben. Vereinzelt sind Reisezentren aber auch unter der Woche geschlossen, wobei Donnerstag der Tag zu sein scheint, an dem die meisten Reisezentren geöffnet haben.
Bei nährerer Betrachtung sieht man allerdings auch, dass große Knotenpunkte wie Mannheim, München, Hamburg oder Berlin durchgehend geöffnet haben. Hierbei ist die von Plotly mitgelieferte Zoom-Funktion hilfreich.
Leider wird dabei die Schriftgröße nicht ebenfalls gezoomt, daher ist die Anzeige der Y-Axe schwierig. Für eine bessere Lesbarkeit, bietet es sich an, das Overlay nutzen.
custom_color_scale = [
[0.0, 'rgb(255, 0, 0)'], # closed = red
[1.0, 'rgb(0, 255, 0)'] # open = green
]
fig = go.Figure(data=go.Heatmap(
z=[[col for col in row] for _, row in df_opening_hours.iterrows()],
x=df_opening_hours.columns,
y=df_travel_center.iloc[df_opening_hours.index]['name'],
xgap=15,
ygap=1,
colorscale=custom_color_scale,
hoverongaps=False
#colorbar_thickness = 10
))
fig.update_layout(
title='Öffnungszeiten Reisezentren',
height=750
)
fig.update_xaxes(title_text='Wochentage',tickson='labels')
fig.update_yaxes(visible=False) # , tickfont = dict(size=4)
fig.update_traces(showscale=False)
fig.show()Unable to display output for mime type(s): application/vnd.plotly.v1+json
Hier lassen sich noch deutlicher die Schließ- und Öffnungszeiten erkennen. Allerdings unterschieden sich Werktage kaum voneinander, sodass es sich hierbei auch um Ungenauigkeiten in den Daten handeln kann.
df_opening_hours_grouped = df_opening_hours.sum().sort_values()
px.bar(df_opening_hours_grouped)Unable to display output for mime type(s): application/vnd.plotly.v1+json
Nun können noch die Informationen der bahnhofsnahmen Dienstleistungen der Karte hinzugefügt werden.
Dazu müssen die Dataframes zunächst wieder zusammengebracht werden.
df_station_facilities_by_id = df_local_services.groupby('id')['type'].unique().reset_index()
df_station_facilities_by_id.head(1)| id | type | |
|---|---|---|
| 0 | 1 | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
df_station_facilities_by_id['id'] = df_station_facilities_by_id['id'].astype(int)# del map_df_extendedmap_df_extended = pd.merge(left=map_df, right=df_station_facilities_by_id, on=['id'], how='left')map_df_extended.head(1)| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | type | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.7678 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
Da in einigen Reihen von type NaN-Werte vorhanden sind, müssen diese zunächst ausgetauscht werden. Immer wenn ein Wert nicht vom Typ Numpy Array ist (also NaN), wird eine neue leere Liste erstellt, um keine Daten zu verlieren.
def replace_nan_with_empty_array(value):
if isinstance(value, np.ndarray):
return value
else:
return np.array([])map_df_extended['type'] = map_df_extended['type'].apply(replace_nan_with_empty_array)
map_df_extended| id | city | countryCode | houseNumber | latitude | longitude | metropolis | name | organisationalUnit | owner | postalCode | state | stationCategory | street | availableTransports | transportAssociations | image | type | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Aachen | DE | 2a | 50.767800 | 6.091499 | {} | Aachen Hbf | RB West | DB S&S | 52064 | Nordrhein-Westfalen | CATEGORY_2 | Bahnhofstr. | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS, HIGH_SP... | [AAV, VRS] | https://api.railway-stations.org/photos/de/1_1... | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, RAILW... |
| 1 | 2 | Aachen | DE | 48 | 50.770202 | 6.116475 | {} | Aachen-Rothe Erde | RB West | DB S&S | 52066 | Nordrhein-Westfalen | CATEGORY_4 | Beverstr. | [REGIONAL_TRAIN, BUS, CITY_TRAIN] | [AAV, VRS] | https://api.railway-stations.org/photos/de/2_1... | [CAR_PARKING, BICYCLE_PARKING, TRIPLE_S_CENTER... |
| 2 | 3 | Aachen | DE | 1 | 50.780360 | 6.070715 | {} | Aachen West | RB West | DB S&S | 52072 | Nordrhein-Westfalen | CATEGORY_5 | Republikplatz | [REGIONAL_TRAIN, BUS] | [AAV, VRS] | https://api.railway-stations.org/photos/de/3_1... | [TRIPLE_S_CENTER, CAR_PARKING, BICYCLE_PARKING... |
| 3 | 4 | Aalen | DE | 1 | 48.841013 | 10.096271 | {} | Aalen Hbf | RB Südwest | DB S&S | 73430 | Baden-Württemberg | CATEGORY_3 | Am Bahnhof | [INTERCITY_TRAIN, REGIONAL_TRAIN, BUS] | [OAM] | https://api.railway-stations.org/photos/de/4.jpg | [MOBILE_TRAVEL_SERVICE, TRIPLE_S_CENTER, CAR_P... |
| 4 | 5 | Abensberg | DE | 27 | 48.819456 | 11.846620 | {} | Abensberg | RB Süd | DB S&S | 93326 | Bayern | CATEGORY_6 | Bahnhofstr. | [REGIONAL_TRAIN] | [] | https://api.railway-stations.org/photos/de/5_1... | [TRIPLE_S_CENTER, PUBLIC_RESTROOM, TAXI_RANK, ... |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 5716 | 8361 | Thalheim | DE | NaN | 50.696652 | 12.849032 | {} | Thalheim (Erzgeb) Mitte | Erzgebirgsbahn (EGB) | DB Regio-Netze | 09380 | Sachsen | CATEGORY_6 | Salzstraße | [REGIONAL_TRAIN, BUS] | [VMS] | NaN | [] |
| 5717 | 8363 | Herten | DE | 35 | 51.597508 | 7.139053 | {} | Herten (Westf) | RB West | DB S&S | 45699 | Nordrhein-Westfalen | CATEGORY_5 | Gartenstr. | [BUS, CITY_TRAIN] | [VRR] | NaN | [] |
| 5718 | 8375 | Rövershagen | DE | 41 | 54.160000 | 12.238300 | {} | Rövershagen Karls Erlebnisdorf (Purkshof) | RB Ost | DB S&S | 18182 | Mecklenburg-Vorpommern | CATEGORY_7 | Purkshof | [REGIONAL_TRAIN] | [VVW] | NaN | [] |
| 5719 | 8388 | Eutingen/Gäu | DE | 20 | 48.484700 | 8.753100 | {} | Eutingen Nord | RB Südwest | DB S&S | 72184 | Baden-Württemberg | CATEGORY_6 | Göttelinger Str. | [REGIONAL_TRAIN, BUS, CITY_TRAIN] | [VGF] | NaN | [] |
| 5720 | 8459 | Düsseldorf | DE | 120 | 51.278517 | 6.766979 | {} | Düsseldorf Flughafen Terminal | RB West | DB S&S | 40474 | Nordrhein-Westfalen | CATEGORY_4 | Flughafenstraße | [REGIONAL_TRAIN, CITY_TRAIN] | [VRS, VRR] | NaN | [] |
5721 rows × 18 columns
Jetzt wird wieder für jeden Typ eine FeatureGroup für die Karte erstellt.
for val in map_df_extended['type'][0]:
print(val)MOBILE_TRAVEL_SERVICE
TRIPLE_S_CENTER
RAILWAY_MISSION
HANDICAPPED_TRAVELLER_SERVICE
LOCKER
WIFI
CAR_PARKING
BICYCLE_PARKING
PUBLIC_RESTROOM
TRAVEL_NECESSITIES
TAXI_RANK
TRAVEL_CENTER
LOST_PROPERTY_OFFICE
facilities_dict = {}
for rowIndex in map_df_extended.index:
for listValue in map_df_extended['type'][rowIndex]:
facilities_dict.setdefault(listValue, folium.FeatureGroup(name=listValue, show=False, autoZIndex=False))
facilities_dict{'MOBILE_TRAVEL_SERVICE': <folium.map.FeatureGroup at 0x230416727d0>,
'TRIPLE_S_CENTER': <folium.map.FeatureGroup at 0x23041673250>,
'RAILWAY_MISSION': <folium.map.FeatureGroup at 0x23041672e30>,
'HANDICAPPED_TRAVELLER_SERVICE': <folium.map.FeatureGroup at 0x23041672d10>,
'LOCKER': <folium.map.FeatureGroup at 0x23041672e60>,
'WIFI': <folium.map.FeatureGroup at 0x23041672fb0>,
'CAR_PARKING': <folium.map.FeatureGroup at 0x23041672ef0>,
'BICYCLE_PARKING': <folium.map.FeatureGroup at 0x230416730a0>,
'PUBLIC_RESTROOM': <folium.map.FeatureGroup at 0x230416728c0>,
'TRAVEL_NECESSITIES': <folium.map.FeatureGroup at 0x23041673700>,
'TAXI_RANK': <folium.map.FeatureGroup at 0x230416736d0>,
'TRAVEL_CENTER': <folium.map.FeatureGroup at 0x230416736a0>,
'LOST_PROPERTY_OFFICE': <folium.map.FeatureGroup at 0x23041673670>,
'RAD_PLUS': <folium.map.FeatureGroup at 0x23041673640>,
'INFORMATION_COUNTER': <folium.map.FeatureGroup at 0x23041673610>,
'VIDEO_TRAVEL_CENTER': <folium.map.FeatureGroup at 0x23041673160>,
'CAR_RENTAL': <folium.map.FeatureGroup at 0x230416726b0>,
'TRAVEL_LOUNGE': <folium.map.FeatureGroup at 0x230416730d0>}
# NOTE: This requires python 3.10.1
def GetIconForFacilities(facility):
try:
match facility:
case 'MOBILE_TRAVEL_SERVICE':
return folium.Icon(color='lightblue', icon='map-marker')
case 'TRIPLE_S_CENTER':
return folium.Icon(color='red', icon='map-marker')
case 'RAILWAY_MISSION':
return folium.Icon(color='darkpurple', icon='map-marker')
case 'HANDICAPPED_TRAVELLER_SERVICE':
return folium.Icon(color='lightgray', icon='map-marker')
case 'CAR_PARKING':
return folium.Icon(color='gray', icon='map-marker')
case 'PUBLIC_RESTROOM':
return folium.Icon(color='cadetblue', icon='map-marker')
case 'TAXI_RANK':
return folium.Icon(color='beige', icon='map-marker')
case 'LOST_PROPERTY_OFFICE':
return folium.Icon(color='white', icon='map-marker')
case 'CAR_RENTAL':
return folium.Icon(color='black', icon='map-marker')
case _:
return folium.Icon(color='blue', icon='map-marker')
except:
return folium.Icon(color='blue', icon='map-marker')Letztlich werden beide Feature Groups, die der Bundesländer und die der Einrichtungen, zusammen auf die Karte gebracht.
Erst werden alle Marker den Feature Groups hinzugefügt, anschließend werden die Feature Groups der Karte hinzugefügt.
Hieraus ergibt sich das Problem, dass sich aufgrund der gleichen Marker an der gleichen Lokation Überschneidungen ergeben. Darüber hinaus kann das Laden je nach Computerressourcen länger dauern oder die Karte nicht korrekt gerendert werden.
Um diesem Problem entgegenzuwirken, kann das Folium Plugin MarkerCluster genutzt werden, welches nahe Marker gruppiert.
Zoomt man in die Karte, werden diese aufgelöst. Sind Marker auf der exakt gleichen Stelle, können durch einen Klick darauf alle Marker betrachtet werden.
Auf diese Weise lassen sich performant alle Marker gleichzeitig auf der Karte anzeigen.
Besonders viele und damit performance-intensive Werte sind TRIPE_S_CENTER, CAR_PARKING und BYICYLE_PARKING. Es gibt aber auch viele PUBLIC_RESTROOM, TRAVEL_NECESSITIES und TAXI_RANK.
map_df = map_df_extended
m = folium.Map(location=[50.111, 8.682],zoom_start=6) # limit with width=1500,height=1500 produces just white space around the map.
cluster = plugins.MarkerCluster(name='Deutschland')
cluster.add_to(m)
for i in map_df.index:
html=f"""
<img src="{map_df['image'][i]}" width="500px">
<br/>
<b><p>{map_df['id'][i]}: {map_df['name'][i]}</b></p>
<p>Transports: {map_df['availableTransports'][i]}</p>
<p>Associations: {map_df['transportAssociations'][i]}</p>
<p>Local services: {map_df['type'][i]}</p>
"""
parsedHtml = folium.Html(html, script=True)
popup = folium.Popup(parsedHtml, max_width=2650)
feature_group_state = state_dict[map_df['state'][i]]
marker = folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIcon(map_df['availableTransports'][i]),
radius=8,
tooltip=map_df['name'][i],
popup=popup
)
marker.add_to(feature_group_state)
marker.add_to(cluster)
for listValue in map_df_extended['type'][i]:
feature_group_type = facilities_dict[listValue]
marker = folium.Marker(
location=[ map_df['latitude'][i], map_df['longitude'][i] ],
icon=GetIconForFacilities(listValue),
radius=8,
tooltip=map_df['name'][i] + ' - ' + listValue
)
marker.add_to(feature_group_type)
marker.add_to(cluster)for fg in state_dict.values():
m.add_child(fg)
for fg in facilities_dict.values():
m.add_child(fg)
folium.LayerControl(collapsed=False).add_to(m)
m